/*
The MIT License

Copyright (c) 2011 Sean M. Thomas

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

#include "fs_Object.h"

#include "assimp.hpp"
#include "aiPostProcess.h"

#define NOMINMAX

#include <gtc/matrix_transform.hpp>
#include <gtx/rotate_vector.hpp>

#include <iostream>
#include <limits>
#include "SOIL.h"
#include "fs_TexturePool.h"
#include "fs_IDPool.h"
using namespace std;

#include "fs_SSFlags.h"

#define FS_AI_PROCESS_FLAGS (aiProcess_OptimizeGraph | aiProcess_OptimizeMeshes | aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_TransformUVCoords | aiProcess_GenUVCoords | aiProcess_FindInvalidData | /*aiProcess_FindDegenerates |*/ aiProcess_SortByPType | aiProcess_FixInfacingNormals | aiProcess_RemoveRedundantMaterials | aiProcess_ValidateDataStructure | aiProcess_PreTransformVertices | /*aiProcess_JoinIdenticalVertices |*/ aiProcess_CalcTangentSpace)

const vec3 defaultColor(1.0f, 1.0f, 1.0f);

/*
// Done by ASSIMP
mat4 findUnitizeMatrix(const aiScene *scene) {
	vec3 maxLoc = vec3(numeric_limits<float>::min());
	vec3 minLoc = vec3(numeric_limits<float>::max());

	vec3 currentVertex;
	for(unsigned int i = 0; i < scene->mNumMeshes; ++i) {
		for(unsigned int j = 0; j < scene->mMeshes[i]->mNumVertices; ++j) {
			currentVertex = vec3(scene->mMeshes[i]->mVertices[j].x, scene->mMeshes[i]->mVertices[j].y, scene->mMeshes[i]->mVertices[j].z);
			
			if(maxLoc.x < currentVertex.x)
				maxLoc.x = currentVertex.x;
			if(maxLoc.y < currentVertex.y)
				maxLoc.y = currentVertex.y;
			if(maxLoc.z < currentVertex.z)
				maxLoc.z = currentVertex.z;

			if(minLoc.x > currentVertex.x)
				minLoc.x = currentVertex.x;
			if(minLoc.y > currentVertex.y)
				minLoc.y = currentVertex.y;
			if(minLoc.z > currentVertex.z)
				minLoc.z = currentVertex.z;
		}
	}

	vec3 center = maxLoc + minLoc;
	center = center*.5f;

	vec3 scale = maxLoc - minLoc;
	scale = vec3(1/scale.x, 1/scale.y, 1/scale.z);

	mat4 unitize_matrix = gtc::matrix_transform::translate(mat4(1.0f), -center);
	unitize_matrix = gtc::matrix_transform::scale(unitize_matrix, scale);
	return unitize_matrix;
}
*/

// Redered unnecessary by aiProcess_PreTransformVertices
 /*
mat4 glmFromAiM(aiMatrix4x4 aMat) {
	return mat4(aMat.a1, aMat.b1, aMat.c1, aMat.d1, aMat.a2, aMat.b2, aMat.c2, aMat.d2, aMat.a3, aMat.b3, aMat.c3, aMat.d3, aMat.a4, aMat.b4, aMat.c4, aMat.d4);
}

void collectMeshTransforms(unsigned int meshNumber, aiNode *n, vector<mat4> &transforms, mat4 currentTransform) {
	currentTransform = currentTransform * glmFromAiM(n->mTransformation);

	// Check for occurence in this node
	for(unsigned int i = 0; i < n->mNumMeshes; ++i)
		if(n->mMeshes[i] == meshNumber)
			transforms.push_back(currentTransform);

	// Now recurse through children occurences in children
	for(unsigned int i = 0; i < n->mNumChildren; ++i)
		collectMeshTransforms(meshNumber, n->mChildren[i], transforms, currentTransform);
}
*/

fs_Object::fs_Object(fs_Object *existingObject) {
	numCopies = existingObject->numCopies;
	++(*numCopies);

	meshes = existingObject->meshes;
	model = existingObject->model;
	textureCount = existingObject->textureCount;

	lastModel = model;
	currentModel = model;
	animated = false;
	ID = fs_IDPool::Instance()->getID();
	ssFlags = SS_FLAG_NOT_SKY;
}

fs_Object::fs_Object(fs_device_mesh deviceMesh) {
	numCopies = new int;
	*numCopies = 1;

	fs_material material;
	meshes.push_back(new fs_Mesh(deviceMesh, material));

	model = mat4(1.0f);
	textureCount = 0;

	lastModel = model;
	currentModel = model;
	animated = false;
	ID = fs_IDPool::Instance()->getID();
	ssFlags = SS_FLAG_NOT_SKY;
}

fs_Object::fs_Object(const char *filePath, const char *fileName) {

	numCopies = new int;
	*numCopies = 1;

	textureCount = 0;

	string fullPath(filePath);
	fullPath.append(fileName);
	cout << "Loading: " << fullPath << endl;

	model = mat4(1.0f);

	Assimp::Importer importer;
	importer.SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_POINT | aiPrimitiveType_LINE);
	importer.SetPropertyInteger(AI_CONFIG_PP_PTV_NORMALIZE, true);

	const aiScene *scene = importer.ReadFile(fullPath.c_str(), FS_AI_PROCESS_FLAGS);

	if(scene == NULL) {
		cout << "Failed to load file.\n";
		return;
	}
	cout << "File parsing complete. Generating openGL VBOs.\n";

	// TODO: Process the loaded data
	// Create all the necessary meshes
	if(scene->HasMeshes()) {

		for(unsigned int i = 0; i < scene->mNumMeshes; ++i) {
			loadMeshFromAiScene(i, scene, string(filePath));
			if(scene->mNumMeshes > 10 && (i % 100) == 0 && i != 0)
				cout << "Processed " << i << " of " << scene->mNumMeshes << " meshes.\n";
		}

		cout << "Created: " << meshes.size() << " meshes. Loaded " << textureCount << " textures.\n";
	}

	cout << "Freeing...(may take time for larger objects)\n";

	lastModel = model;
	currentModel = model;
	animated = false;
	ID = fs_IDPool::Instance()->getID();
	ssFlags = SS_FLAG_NOT_SKY;
}

fs_Object::~fs_Object(void) {
	--(*numCopies);

	// Delete the meshes if no more copies are around
	if(*numCopies < 1) {
		while(!meshes.empty()) {
			delete meshes.back();
			meshes.pop_back();
		}

		delete numCopies;
		numCopies = NULL;
	}

	fs_IDPool::Instance()->releaseID(ID);
	lastModel = model;
}

// Because of flags, all faces are gaurunteed to be triangular
void fs_Object::loadMeshFromAiScene(unsigned int meshNumber, const aiScene *scene, string filePath) {
	aiMesh *currentMesh = scene->mMeshes[meshNumber];
	fs_device_mesh meshData;
	meshData.num_indices = 0; // Stop compiler warning
	fs_device_mesh_initToZero(meshData);

	// Can't do much without positions and if there are no faces
	if(!currentMesh->HasPositions() || currentMesh->mNumFaces < 1)
		return;

	// Positions are included, create the vertex array
	glGenVertexArrays(1, &(meshData.vertex_array));
	glBindVertexArray(meshData.vertex_array);

	// Allocate VBO for the positions and indices
	glGenBuffers(1,&(meshData.vbo_indices));
	glGenBuffers(1,&(meshData.vbo_vertices));

	// Calculate the number of triangle faces
	unsigned int numTriFaces = currentMesh->mNumFaces;

	// Read the indices from the mesh
	GLushort *indices = new GLushort[3*numTriFaces];
	meshData.num_indices = 3*numTriFaces;
	for(unsigned int i = 0; i < currentMesh->mNumFaces; ++i) {
		indices[3*i] = (GLushort)(currentMesh->mFaces[i].mIndices[0]);
		indices[3*i + 1] = (GLushort)(currentMesh->mFaces[i].mIndices[1]);
		indices[3*i + 2] = (GLushort)(currentMesh->mFaces[i].mIndices[2]);
	}

	// Bind the buffer and upload the index data
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, meshData.vbo_indices);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, 3*numTriFaces*sizeof(GLushort), indices, GL_STATIC_DRAW);
	delete indices;

	// Read the vertex data from the mesh
	float *vertices = new float[currentMesh->mNumVertices*3];
	for(unsigned int i = 0; i < currentMesh->mNumVertices; ++i) {
			vertices[3*i] = currentMesh->mVertices[i].x;
			vertices[3*i + 1] = currentMesh->mVertices[i].y;
			vertices[3*i + 2] = currentMesh->mVertices[i].z;
	}

	// Bind the buffer and upload the vertex data
	glBindBuffer(GL_ARRAY_BUFFER, meshData.vbo_vertices);
	glBufferData(GL_ARRAY_BUFFER, currentMesh->mNumVertices*3*sizeof(float), vertices, GL_STATIC_DRAW);
	glVertexAttribPointer(fs_vertex_attribute::POSITION, 3, GL_FLOAT, GL_FALSE, 0, 0);
	glEnableVertexAttribArray(fs_vertex_attribute::POSITION);
	delete vertices;

	// Check for normals
	if(currentMesh->HasNormals()) {
		// Allocate a VBO for the normals
		glGenBuffers(1,&(meshData.vbo_normals));

		// Read the normals from the mesh
		float *normals = new float[currentMesh->mNumVertices*3];
		for(unsigned int i = 0; i < currentMesh->mNumVertices; ++i) {
			normals[3*i] = currentMesh->mNormals[i].x;
			normals[3*i + 1] = currentMesh->mNormals[i].y;
			normals[3*i + 2] = currentMesh->mNormals[i].z;
		}

		// Bind the buffer and upload the vertex data
		glBindBuffer(GL_ARRAY_BUFFER, meshData.vbo_normals);
		glBufferData(GL_ARRAY_BUFFER, currentMesh->mNumVertices*3*sizeof(float), normals, GL_STATIC_DRAW);
		glVertexAttribPointer(fs_vertex_attribute::NORMAL, 3, GL_FLOAT, GL_FALSE, 0, 0);
		glEnableVertexAttribArray(fs_vertex_attribute::NORMAL);
		delete normals;
	}
	else {
		cout << "No normals available for mesh " << meshNumber << ". Discarding.\n";
		meshData.vbo_normals = 0;
		fs_material material;
		delete (new fs_Mesh(meshData, material));
		return;
	}
	
	// Load the material properties
	fs_material material;
	aiColor4D color(0.0f, 0.0f, 0.0f, 0.0f);
	aiMaterial *aiMat = scene->mMaterials[currentMesh->mMaterialIndex];
	if(AI_SUCCESS == aiGetMaterialColor(aiMat, AI_MATKEY_COLOR_DIFFUSE, &color)) {
		material.kAmbient.r = color.r;
		material.kAmbient.g = color.g;
		material.kAmbient.b = color.b;
	}
	else {
		material.kAmbient.r = defaultColor.r;
		material.kAmbient.g = defaultColor.g;
		material.kAmbient.b = defaultColor.b;
	}
	if(AI_SUCCESS != aiGetMaterialFloat(aiMat, AI_MATKEY_SHININESS, &(material.specExp)))
		material.specExp = 0.0f;
	


	// Check for a texture
	if(currentMesh->HasTextureCoords(0) && aiGetMaterialTextureCount(aiMat,aiTextureType_DIFFUSE)) {
		// Try to load the texture
		aiString *txFileLoc = new aiString();
		aiGetMaterialTexture(aiMat,aiTextureType_DIFFUSE,0,txFileLoc,0,0,0,0,0);
		filePath.append(txFileLoc->data);
		GLuint textureID = fs_TexturePool::Instance()->loadTexture(filePath);

		// Check to see if it is a valid textureID
		if(textureID != 0) {
			meshData.textureID = textureID;

			// Load the UV coordinates
			float *uv = new float[currentMesh->mNumVertices*2];
			for(unsigned int i = 0; i < currentMesh->mNumVertices; ++i) {
				uv[2*i] = currentMesh->mTextureCoords[0][i].x;
				uv[2*i + 1] = currentMesh->mTextureCoords[0][i].y;
			}
			// Create the VBO
			glGenBuffers(1,&(meshData.vbo_txCoord));
			// Bind the buffer and upload the vertex data
			glBindBuffer(GL_ARRAY_BUFFER, meshData.vbo_txCoord);
			glBufferData(GL_ARRAY_BUFFER, currentMesh->mNumVertices*2*sizeof(float), uv, GL_STATIC_DRAW);
			glVertexAttribPointer(fs_vertex_attribute::TEXCOORD, 2, GL_FLOAT, GL_FALSE, 0, 0);
			glEnableVertexAttribArray(fs_vertex_attribute::TEXCOORD);
			++textureCount;
			delete uv;
		}
		else
			cout << "Unable to load texture: " << filePath << endl;

		delete txFileLoc;
	}
	// Check for per-vertex colors
	if(meshData.textureID == 0) {
		if(currentMesh->HasVertexColors(0)) {
			float *vertexColor = new float[3*currentMesh->mNumVertices];
			for(unsigned int i = 0; i < currentMesh->mNumVertices; ++i) {
				vertexColor[3*i] = currentMesh->mColors[0][i].r;
				vertexColor[3*i + 1] = currentMesh->mColors[0][i].g;
				vertexColor[3*i + 2] = currentMesh->mColors[0][i].b;
			}
			// Create the VBO and upload the data
			glGenBuffers(1,&(meshData.vbo_colors));
			glBindBuffer(GL_ARRAY_BUFFER, meshData.vbo_colors);
			glBufferData(GL_ARRAY_BUFFER, currentMesh->mNumVertices*3*sizeof(float), vertexColor, GL_STATIC_DRAW);
			glVertexAttribPointer(fs_vertex_attribute::COLOR, 3, GL_FLOAT, GL_FALSE, 0, 0);
			glEnableVertexAttribArray(fs_vertex_attribute::COLOR);
			delete vertexColor;
		}
		// Add the diffuse material color as a per-vertex color
		else {
			float *vertexColor = new float[3*currentMesh->mNumVertices];
			for(unsigned int i = 0; i < currentMesh->mNumVertices; ++i) {
				vertexColor[3*i] = material.kAmbient.r;
				vertexColor[3*i + 1] = material.kAmbient.g;
				vertexColor[3*i + 2] = material.kAmbient.b;
			}
			// Create the VBO and upload the data
			glGenBuffers(1,&(meshData.vbo_colors));
			glBindBuffer(GL_ARRAY_BUFFER, meshData.vbo_colors);
			glBufferData(GL_ARRAY_BUFFER, currentMesh->mNumVertices*3*sizeof(float), vertexColor, GL_STATIC_DRAW);
			glVertexAttribPointer(fs_vertex_attribute::COLOR, 3, GL_FLOAT, GL_FALSE, 0, 0);
			glEnableVertexAttribArray(fs_vertex_attribute::COLOR);
			delete vertexColor;
		}
	}

	meshes.push_back(new fs_Mesh(meshData, material));
}

void fs_Object::setModelTransform(mat4 newModel) {
	model = newModel;
}

void fs_Object::draw(fs_first_pass_draw_options settings) {
	mat3 linearTransform, inverse_transposed;

	if(animated)
		currentModel = animModel * model;
	else {
		currentModel = model;
		lastModel = model;
	}

	linearTransform = mat3(settings.view * currentModel);
	inverse_transposed = transpose(inverse(linearTransform));

	// Set uniforms for both shader types
	glUseProgram(settings.sC.txShader.shader);
	glUniformMatrix3fv(settings.sC.txShader.uniforms[U_INV_TRANS], 1, GL_FALSE, &inverse_transposed[0][0]);
	glUniformMatrix4fv(settings.sC.txShader.uniforms[U_MODEL], 1, GL_FALSE, &currentModel[0][0]);
	glUniformMatrix4fv(settings.sC.txShader.uniforms[U_LAST_MODEL], 1, GL_FALSE, &lastModel[0][0]);
	glUniform1ui(settings.sC.txShader.uniforms[U_OBJECT_ID], ID);
	glUniform1ui(settings.sC.txShader.uniforms[U_FLAGS], ssFlags);
	glUseProgram(settings.sC.vtShader.shader);
	glUniformMatrix3fv(settings.sC.vtShader.uniforms[U_INV_TRANS], 1, GL_FALSE, &inverse_transposed[0][0]);
	glUniformMatrix4fv(settings.sC.vtShader.uniforms[U_MODEL], 1, GL_FALSE, &currentModel[0][0]);
	glUniformMatrix4fv(settings.sC.vtShader.uniforms[U_LAST_MODEL], 1, GL_FALSE, &lastModel[0][0]);
	glUniform1ui(settings.sC.vtShader.uniforms[U_OBJECT_ID], ID);
	glUniform1ui(settings.sC.vtShader.uniforms[U_FLAGS], ssFlags);
	
	for(unsigned int i = 0; i < meshes.size(); ++i)
		meshes[i]->draw(settings);	
}

void fs_Object::scale(float scaleFactor, bool append) {
	mat4 xform = gtc::matrix_transform::scale(mat4(1.0f), vec3(scaleFactor, scaleFactor, scaleFactor));

	if(append)
		model = model * xform;
	else
		model = xform * model;
}

void fs_Object::rotate(float angle, vec3 axis, bool append) {
	mat4 xform = gtc::matrix_transform::rotate(mat4(1.0f), angle, axis);

	if(append)
		model = model * xform;
	else
		model = xform * model;
}

void fs_Object::setIdentity() {
	model = mat4(1.0f);
}

void fs_Object::translate(vec3 trans, bool append) {
	mat4 xform = gtc::matrix_transform::translate(mat4(1.0f), trans);

	if(append)
		model = model * xform;
	else
		model = xform * model;
}

void fs_Object::animate(float rotationAngle, float distance, vec3 up, vec3 zeroAngleVector) {
	
	vec3 rotatedVector = gtx::rotate_vector::rotate(zeroAngleVector, rotationAngle, up);
	animModel = gtc::matrix_transform::translate(mat4(1.0), distance * rotatedVector);

	animated = true;
	ssFlags = SS_FLAG_NOT_SKY | SS_FLAG_ANIMATED;
	lastModel = currentModel;
}